#
# Copyright (C) 2019 Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions of
# the GNU General Public License v.2, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY expressed or implied, including the implied warranties of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
# Public License for more details.  You should have received a copy of the
# GNU General Public License along with this program; if not, write to the
# Free Software Foundation, Inc., 31 Milk Street #960789 Boston, MA
# 02196 USA.  Any Red Hat trademarks that are incorporated in the
# source code or documentation are not subject to the GNU General Public
# License and may only be used or replicated with the express permission of
# Red Hat, Inc.
#
import os
import shutil

from pyanaconda.anaconda_loggers import get_module_logger
from pyanaconda.core import service
from pyanaconda.core.configuration.anaconda import conf
from pyanaconda.core.kernel import kernel_arguments
from pyanaconda.core.path import join_paths, make_directories
from pyanaconda.modules.common.errors.installation import NetworkInstallationError
from pyanaconda.modules.common.task import Task
from pyanaconda.modules.network.config_file import IFCFG_DIR, KEYFILE_DIR
from pyanaconda.modules.network.nm_client import (
    commit_changes_with_autoconnection_blocked,
    get_config_file_connection_of_device,
    nm_client_in_thread,
    update_connection_values,
)
from pyanaconda.modules.network.utils import (
    guard_by_system_configuration,
    is_nbft_device,
)

log = get_module_logger(__name__)

import gi

gi.require_version("NM", "1.0")
from gi.repository import NM


class HostnameConfigurationTask(Task):
    """Hostname configuration task."""

    HOSTNAME_CONF_FILE_PATH = "/etc/hostname"

    def __init__(self, sysroot, hostname, overwrite):
        """Create a new task.

        :param sysroot: a path to the root of installed system
        :type sysroot: str
        :param hostname: static hostname
        :type hostname: str
        :param overwrite: overwrite config files if they already exist
        :type overwrite: bool
        """
        super().__init__()
        self._sysroot = sysroot
        self._hostname = hostname
        self._overwrite = overwrite

    @property
    def name(self):
        return "Configure hostname"

    def run(self):
        _write_config_file(
            self._sysroot, self.HOSTNAME_CONF_FILE_PATH,
            "{}\n".format(self._hostname),
            "Cannot write hostname configuration file",
            self._overwrite
        )


def _write_config_file(root, path, content, error_msg, overwrite):
    """Write content into config file on the target system.

    :param root: path to the root of the target system
    :type root: str
    :param path: config file path in target system root
    :type path: str
    :param content: content to be written into config file
    :type content: str
    :param error_msg: error message in case of failure
    :type error_msg: str
    :param overwrite: overwrite existing configuration file
    :type overwrite: bool
    """
    fpath = os.path.normpath(root + path)
    if os.path.isfile(fpath) and not overwrite:
        log.debug("Not overwriting existing configuration file %s", fpath)
        return
    try:
        with open(fpath, "w") as fobj:
            fobj.write(content)
    except OSError as e:
        msg = "{}: {}".format(error_msg, e.strerror)
        raise NetworkInstallationError(msg) from e


class NetworkInstallationTask(Task):
    """Installation task for the network configuration."""

    SYSCONF_NETWORK_FILE_PATH = "/etc/sysconfig/network"
    ANACONDA_SYSCTL_FILE_PATH = "/etc/sysctl.d/anaconda.conf"
    RESOLV_CONF_FILE_PATH = "/etc/resolv.conf"
    NETWORK_SCRIPTS_DIR_PATH = IFCFG_DIR
    PREFIXDEVNAME_CONFIG_FILE_PREFIX = "71-net-ifnames-prefix-"
    NETWORK_SCRIPTS_CONFIG_FILE_PREFIXES = ("ifcfg-", "keys-", "route-")
    DHCLIENT_FILE_TEMPLATE = "/etc/dhcp/dhclient-{}.conf"
    SYSTEMD_NETWORK_CONFIG_DIR = "/etc/systemd/network"
    INTERFACE_RENAME_FILE_TEMPLATE = "10-anaconda-ifname-{}.link"
    NM_SYSTEM_CONNECTIONS_DIR_PATH = KEYFILE_DIR
    NM_GLOBAL_DNS_RUNTIME_CONFIG_DIR = "/run/NetworkManager/conf.d"
    NM_GLOBAL_DNS_CONFIG_DIR = "/etc/NetworkManager/conf.d"
    NM_GLOBAL_DNS_CONFIG_FILES = ("16-global-dns.conf", "16-dns-backend.conf")
    INTERFACE_RENAME_FILE_CONTENT_TEMPLATE = """
# Generated by Anaconda based on ifname= installer boot option.
[Match]
MACAddress={}

[Link]
Name={}
""".strip()
    DNSCONFD_SERVICE = "dnsconfd"

    def __init__(self, sysroot, disable_ipv6, overwrite,
                 network_ifaces, ifname_option_values,
                 configure_persistent_device_names):
        """Create a new task.

        :param sysroot: a path to the root of installed system
        :type sysroot: str
        :param disable_ipv6: disable ipv6 on target system
        :type disable_ipv6: bool
        :param overwrite: overwrite config files if they already exist
        :type overwrite: bool
        :param network_ifaces: list of network interfaces for dhcp configuration
        :type network_ifaces: list(str)
        :param ifname_option_values: list of ifname boot option values
        :type ifname_option_values: list(str)
        :param configure_persistent_device_names: configure persistent network device
                                                  names on target system
        :type configure_persistent_device_names: bool
        """
        super().__init__()
        self._sysroot = sysroot
        self._disable_ipv6 = disable_ipv6
        self._overwrite = overwrite
        self._network_ifaces = network_ifaces
        self._ifname_option_values = ifname_option_values
        self._configure_persistent_device_names = configure_persistent_device_names

    @property
    def name(self):
        return "Configure network"

    def run(self):
        self._write_sysconfig_network(self._sysroot, self._overwrite)
        self._write_interface_rename_config(self._sysroot, self._ifname_option_values,
                                            self._overwrite)
        if self._disable_ipv6:
            self._disable_ipv6_on_system(self._sysroot)
        self._copy_device_config_files(self._sysroot)
        self._copy_dhclient_config_files(self._sysroot, self._network_ifaces)

        # Make sure DNS resolution works in %post scripts
        # when systemd-resolved is not available.
        #
        # Main use-case for this is currently image mode on RHEL.
        # In this case there is no systemd-resolved in use by default and
        # image mode deploys the target system rootfs only after the early
        # resolv.conf copy action has run, clobbering the resolv.conf we copy earlier
        # in the copy_resolv_conf_to_root() method.
        if conf.system.provides_resolver_config and \
                not service.is_service_installed("systemd-resolved.service"):
            self._copy_resolv_conf(self._sysroot, self._overwrite)
        if self._configure_persistent_device_names:
            self._copy_prefixdevname_files(self._sysroot)
        self._copy_global_dns_config(self._sysroot)
        self._enable_dnsconfd(self._sysroot)

    def _write_sysconfig_network(self, root, overwrite):
        """Write empty /etc/sysconfig/network target system configuration file.

        :param root: path to the root of the target system
        :type root: str
        :param overwrite: overwrite existing configuration file
        :type overwrite: bool
        """
        return _write_config_file(root, self.SYSCONF_NETWORK_FILE_PATH,
                                  "# Created by anaconda\n",
                                  "Cannot write {} configuration file".format(
                                      self.SYSCONF_NETWORK_FILE_PATH),
                                  overwrite)

    def _write_interface_rename_config(self, root, ifname_option_values, overwrite):
        """Write systemd configuration .link file for interface renaming.
        :param root: path to the root of the target system
        :type root: str
        :param ifname_option_values: list of ifname boot option values
        :type ifname_option_values: list(str)
        :param overwrite: overwrite existing configuration file
        :type overwrite: bool
        """

        # Avoid link files for 'nbft' interfaces as it breaks Boot from NVM over TCP
        ifname_option_values = [value for value in ifname_option_values
                                if not is_nbft_device(value.split(':')[0])]

        if ifname_option_values:
            target_system_dir = join_paths(root, self.SYSTEMD_NETWORK_CONFIG_DIR)
            make_directories(target_system_dir)

        for ifname_value in ifname_option_values:
            iface, mac = ifname_value.split(":", 1)
            content = self.INTERFACE_RENAME_FILE_CONTENT_TEMPLATE.format(mac, iface)
            config_file = self.INTERFACE_RENAME_FILE_TEMPLATE.format(iface)
            config_file_path = join_paths(self.SYSTEMD_NETWORK_CONFIG_DIR, config_file)
            _write_config_file(
                root,
                config_file_path,
                content,
                "Cannot write {} configuration file for ifname={} option.".format(
                    config_file_path, ifname_value),
                overwrite
            )

    def _disable_ipv6_on_system(self, root):
        """Disable ipv6 on target system.

        :param root: path to the root of the target system
        :type root: str
        """
        fpath = os.path.normpath(root + self.ANACONDA_SYSCTL_FILE_PATH)
        try:
            with open(fpath, "a") as f:
                f.write("# Anaconda disabling ipv6 (noipv6 option)\n")
                f.write("net.ipv6.conf.all.disable_ipv6=1\n")
                f.write("net.ipv6.conf.default.disable_ipv6=1\n")

        except OSError as e:
            msg = "Cannot disable ipv6 on the system: {}".format(e.strerror)
            raise NetworkInstallationError(msg) from e

    def _copy_resolv_conf(self, root, overwrite):
        """Copy resolf.conf file to target system.

        :param root: path to the root of the target system
        :type root: str
        :param overwrite: overwrite existing configuration file
        :type overwrite: bool
        """
        self._copy_file_to_root(root, self.RESOLV_CONF_FILE_PATH, follow_symlinks=False)

    def _copy_file_to_root(self, root, config_file, overwrite=False, follow_symlinks=True):
        """Copy the file to target system.

        :param root: path to the root of the target system
        :type root: str
        :param config_file: path of the file
        :type config_file: str
        :param overwrite: overwrite existing configuration file
        :type overwrite: bool
        """
        if not os.path.isfile(config_file):
            return
        fpath = os.path.normpath(root + config_file)
        if (os.path.isfile(fpath) or os.path.islink(fpath)) and not overwrite:
            return
        if not os.path.isdir(os.path.dirname(fpath)):
            make_directories(os.path.dirname(fpath))
        shutil.copy(config_file, fpath, follow_symlinks=follow_symlinks)

    def _copy_device_config_files(self, root):
        """Copy network device config files to target system.

        :param root: path to the root of the target system
        :type root: str
        """
        if os.path.exists(self.NETWORK_SCRIPTS_DIR_PATH):
            for config_file in os.listdir(self.NETWORK_SCRIPTS_DIR_PATH):
                if config_file.startswith(self.NETWORK_SCRIPTS_CONFIG_FILE_PREFIXES):
                    config_file_path = os.path.join(self.NETWORK_SCRIPTS_DIR_PATH,
                                                    config_file)
                    self._copy_file_to_root(root, config_file_path)
        if os.path.exists(self.NM_SYSTEM_CONNECTIONS_DIR_PATH):
            for config_file in os.listdir(self.NM_SYSTEM_CONNECTIONS_DIR_PATH):
                config_file_path = os.path.join(self.NM_SYSTEM_CONNECTIONS_DIR_PATH,
                                                config_file)
                self._copy_file_to_root(root, config_file_path)

    def _copy_dhclient_config_files(self, root, network_ifaces):
        """Copy dhclient configuration files to target system.

        :param root: path to the root of the target system
        :type root: str
        :param network_ifaces: ifaces whose config files should be copied
        :type network_ifaces: list(str)
        """
        for device_name in network_ifaces:
            dhclient_file = self.DHCLIENT_FILE_TEMPLATE.format(device_name)
            self._copy_file_to_root(root, dhclient_file)

    def _copy_prefixdevname_files(self, root):
        """Copy prefixdevname persistent configuration to target system.

        :param root: path to the root of the target system
        :type root: str
        """
        config_files = os.listdir(self.SYSTEMD_NETWORK_CONFIG_DIR)
        for config_file in config_files:
            if config_file.startswith(self.PREFIXDEVNAME_CONFIG_FILE_PREFIX):
                config_file_path = os.path.join(self.SYSTEMD_NETWORK_CONFIG_DIR,
                                                config_file)
                self._copy_file_to_root(root, config_file_path)

    def _copy_global_dns_config(self, root):
        """Copy NM global dns configuration to target system.

        :param root: path to the root of the target system
        :type root: str
        """
        for file in self.NM_GLOBAL_DNS_CONFIG_FILES:
            src = os.path.join(self.NM_GLOBAL_DNS_RUNTIME_CONFIG_DIR, file)
            dst = os.path.normpath(root + os.path.join(self.NM_GLOBAL_DNS_CONFIG_DIR, file))
            if os.path.isfile(src):
                shutil.copy(src, dst)

    def _enable_dnsconfd(self, root):
        """Enable dnsconfd service if dnsconfd backend is used."""
        if kernel_arguments.get("rd.net.dns-backend") == "dnsconfd":
            if not service.is_service_installed(self.DNSCONFD_SERVICE, root=root):
                log.debug("The dnsconfd service is not installed.")
                return
            service.enable_service(self.DNSCONFD_SERVICE, root=root)

class ConfigureActivationOnBootTask(Task):
    """Task for configuration of automatic activation of devices on boot"""

    def __init__(self, onboot_ifaces):
        """Create a new task.

        :param onboot_ifaces: interfaces that should be autoactivated on boot
        :type onboot_ifaces: list(str)
        """
        super().__init__()
        self._onboot_ifaces = onboot_ifaces

    @property
    def name(self):
        return "Configure automatic activation on boot."

    @guard_by_system_configuration(return_value=None)
    def run(self):
        with nm_client_in_thread() as nm_client:
            return self._run(nm_client)

    def _run(self, nm_client):
        if not nm_client:
            log.debug("%s: No NetworkManager available.", self.name)
            return None

        for iface in self._onboot_ifaces:
            con_uuid = get_config_file_connection_of_device(nm_client, iface)
            if con_uuid:
                con = nm_client.get_connection_by_uuid(con_uuid)
                update_connection_values(
                    con,
                    [("connection", NM.SETTING_CONNECTION_AUTOCONNECT, True)]
                )
                commit_changes_with_autoconnection_blocked(con, nm_client)
            else:
                log.warning("Configure ONBOOT: can't find config for %s", iface)
